#!/bin/sh

# Rationale: Tor needs a somewhat accurate clock to work.
# If the clock is wrong enough to prevent it from opening circuits,
# we set the time to the middle of the valid time interval found
# in the Tor consensus, and we restart it.
# In any case, we use HTP to ask more accurate time information to
# a few authenticated HTTPS servers.

# Get LIVE_USERNAME
. /etc/live/config.d/username.conf

# Import tor_control_*(), tor_is_working(), TOR_LOG, TOR_DIR
. /usr/local/lib/tails-shell-library/tor.sh

### Init variables

TORDATE_DIR=/var/run/tordate
TORDATE_DONE_FILE=${TORDATE_DIR}/done
TOR_CONSENSUS=${TOR_DIR}/cached-microdesc-consensus
TOR_UNVERIFIED_CONSENSUS=${TOR_DIR}/unverified-microdesc-consensus
TOR_UNVERIFIED_CONSENSUS_HARDLINK=${TOR_UNVERIFIED_CONSENSUS}.bak
INOTIFY_TIMEOUT=60
DATE_RE='[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]'
VERSION_FILE=/etc/amnesia/version

### Exit conditions

# Run only when the interface is not "lo":
if [ "$1" = "lo" ]; then
	exit 0
fi

# Run whenever an interface gets "up", not otherwise:
if [ "$2" != "up" ]; then
	exit 0
fi

# Do not run twice
if [ -e "$TORDATE_DONE_FILE" ]; then
	exit 0
fi


### Create status directory
install -o root -g root -m 0755 -d ${TORDATE_DIR}


### Functions

log() {
	logger -t time "$@"
}

notify_user() {
	local summary="$1"
	local body="$2"

	if [ -n "$3" ]; then
		timeout_args='--expire-time=$3'
	fi
	export DISPLAY=':0.0'
	export XAUTHORITY="`echo /var/run/gdm3/auth-for-${LIVE_USERNAME}-*/database`"
	exec /bin/su -c "notify-send ${timeout_args} \"${summary}\" \"${body}\"" "${LIVE_USERNAME}" &
}

has_consensus() {
	local files="${TOR_CONSENSUS} ${TOR_UNVERIFIED_CONSENSUS}"

	if [ $# -ge 1 ]; then
		files="$@"
	fi
	grep -qs "^valid-until ${DATE_RE}"'$' ${files}
}

has_only_unverified_consensus() {
	[ ! -e ${TOR_CONSENSUS} ] && has_consensus ${TOR_UNVERIFIED_CONSENSUS}
}

wait_for_tor_consensus_helper() {
	tries=0
	while ! has_consensus && [ $tries -lt 5 ]; do
		inotifywait -q -t 30 -e close_write -e moved_to ${TOR_DIR} || log "timeout"
		tries=$(expr $tries + 1)
	done

	# return some kind of success measurement
	has_consensus
}

wait_for_tor_consensus() {
	log "Waiting for a Tor consensus file to contain a valid time interval"
	if ! has_consensus && ! wait_for_tor_consensus_helper; then
		log "Unsuccessfully waited for Tor consensus, restarting Tor and retrying."
		restart-tor
	fi
	if ! has_consensus && ! wait_for_tor_consensus_helper; then
		log "Unsuccessfully retried waiting for Tor consensus, aborting."
	fi
	if has_consensus; then
		log "A Tor consensus file now contains a valid time interval."
	else
		log "Waited for too long, let's stop waiting for Tor consensus."
		# FIXME: gettext-ize
		notify_user "Synchronizing the system's clock" \
			"Could not fetch Tor consensus."
		exit 2
	fi
}

wait_for_working_tor() {
	log "Waiting for Tor to be working (i.e. cached descriptors exist)..."
	while ! tor_is_working; do
		inotifywait -q -t ${INOTIFY_TIMEOUT} -e close_write -e moved_to ${TOR_DIR} || log "timeout"
	done
	log "Tor is now working."
}

date_points_are_sane() {
	local vstart="$1"
	local vend="$2"

	vendchk=$(date -ud "${vstart} -0300" +'%F %T')
	[ "${vend}" = "${vendchk}" ]
}

time_is_in_valid_tor_range() {
	local curdate="$1"
	local vstart="$2"

	vendcons=$(date -ud "${vstart} -0230" +'%F %T')
	order="${vstart}
${curdate}
${vendcons}"
	ordersrt=$(echo "${order}" | sort)

	[ "${order}" = "${ordersrt}" ]
}

maybe_set_time_from_tor_consensus() {
	local consensus=${TOR_CONSENSUS}

	if has_only_unverified_consensus \
	   && ln -f ${TOR_UNVERIFIED_CONSENSUS} ${TOR_UNVERIFIED_CONSENSUS_HARDLINK}; then
		consensus=${TOR_UNVERIFIED_CONSENSUS_HARDLINK}
		log "We do not have a Tor verified consensus, let's use the unverified one."
	fi

	log "Waiting for the chosen Tor consensus file to contain a valid time interval..."
	while ! has_consensus ${consensus}; do
		inotifywait -q -t ${INOTIFY_TIMEOUT} -e close_write -e moved_to ${TOR_DIR} || log "timeout"
	done
	log "The chosen Tor consensus now contains a valid time interval, let's use it."


	# Get various date points in Tor's format, and do some sanity checks
	vstart=$(sed -n "/^valid-after \(${DATE_RE}\)"'$/s//\1/p; t q; b; :q q' ${consensus})
	vend=$(sed -n "/^valid-until \(${DATE_RE}\)"'$/s//\1/p; t q; b; :q q' ${consensus})
	vmid=$(date -ud "${vstart} -0130" +'%F %T')
	log "Tor: valid-after=${vstart} | valid-until=${vend}"

	if ! date_points_are_sane "${vstart}" "${vend}"; then
		log "Unexpected valid-until: [${vend}] is not [${vstart} + 3h]"
		return
	fi

	curdate=$(date -u +'%F %T')
	log "Current time is ${curdate}"

	if time_is_in_valid_tor_range "${curdate}" "${vstart}"; then
		log "Current time is in valid Tor range"
		return
	fi

	log "Current time is not in valid Tor range, setting to middle of this range: [${vmid}]"
	date -us "${vmid}" 1>/dev/null

	# Tor is unreliable with picking a circuit after time change
	restart-tor
}

tor_cert_valid_after() {
	# Only print the last = freshest match
	sed -n 's/^.*certificate lifetime runs from \(.*\) through.*$/\1/p' \
	    ${TOR_LOG} | tail -n 1
}

tor_bootstrap_progress() {
	grep -o "\[notice\] Bootstrapped [[:digit:]]\+%:" ${TOR_LOG} | \
	    tail -n1 | sed "s|\[notice\] Bootstrapped \([[:digit:]]\+\)%:|\1|"
}

tor_cert_lifetime_invalid() {
	# To be sure that we only grep relevant information, we
	# should delete the log when Tor is started, which we do
	# in 10-tor.sh.
	# The log severity will be "warn" if bootstrapping with
	# authorities and "info" with bridges.
	grep -q "\[\(warn\|info\)\] Certificate \(not yet valid\|already expired\)\." \
	    ${TOR_LOG}
}

# This check is blocking until Tor reaches either of two states:
# 1. Tor completes a handshake with an authority (or bridge).
# 2. Tor fails the handshake with all authorities (or bridges).
# Since 2 essentially is the negation of 1, one of them will happen,
# so it won't block forever. Hence we shouldn't need a timeout.
# FIXME: An exception would be if Tor has DisableNetwork=1, which we
# will use once we fully support bridge mode, so we will have to
# revisit this then.
is_clock_way_off() {
	log "Checking if system clock is way off"
	until [ "$(tor_bootstrap_progress)" -gt 10 ]; do
		if tor_cert_lifetime_invalid; then
			return 0
		fi
		sleep 1
	done
	return 1
}

start_notification_helper() {
	export DISPLAY=':0.0'
	export XAUTHORITY="$(echo /var/run/gdm3/auth-for-$LIVE_USERNAME-*/database)"
	exec /bin/su -c /usr/local/bin/tails-htp-notify-user "$LIVE_USERNAME" &
}


### Main

start_notification_helper

# Delegate time setting to other daemons if Tor connections work
if tor_is_working; then
	log "Tor has already opened a circuit"
else
	# Since Tor 0.2.3.x Tor doesn't download a consensus for
	# clocks that are more than 30 days in the past or 2 days in
	# the future.  For such clock skews we set the time to the
	# authority's cert's valid-after date.
	if is_clock_way_off; then
		log "The clock is so badly off that Tor cannot download a consensus. Setting system time to the authority's cert's valid-after date and trying to fetch a consensus again..."
		date --set="$(tor_cert_valid_after)" > /dev/null
		service tor reload
	fi
	wait_for_tor_consensus
	maybe_set_time_from_tor_consensus
fi

wait_for_working_tor

# Disable "info" logging workaround from 10-tor.sh
if grep -qw bridge /proc/cmdline; then
	tor_control_setconf "Log=\"notice file ${TOR_LOG}\""
fi

touch $TORDATE_DONE_FILE

log "Restarting htpdate"
service htpdate restart
log "htpdate service restarted with return code $?"
